package fabricsdk_go

import (
	"context"
	"crypto/tls"
	"fmt"
	"gitee.com/wayjin/fabricsdk-go/cryptoutil"
	"github.com/hyperledger/fabric/common/util"
	"github.com/hyperledger/fabric/core/comm"
	"github.com/hyperledger/fabric/peer/common"
	"github.com/hyperledger/fabric/peer/common/api"
	ab "github.com/hyperledger/fabric/protos/orderer"
	pb "github.com/hyperledger/fabric/protos/peer"
	"github.com/pkg/errors"
	"time"
)

type PeerConfig struct {
	Host    string
	ServiceOverrideName string
	RootTLSCert  []byte
	ConnTimeout  int32
}

type commonClient struct {
	*comm.GRPCClient
	address string
	sn      string
}

func (conf PeerConfig) client() (*peerClient, error) {
	return newPeerClientWithTLSBytes(conf.Host, conf.ServiceOverrideName, conf.RootTLSCert)
}

func (c PeerConfig) Client() (*peerClient, error) {
	return c.client()
}
func newPeerClientWithTLSBytes(address, sn string, tlsRootBytes []byte) (*peerClient, error) {
	clientConfig := comm.ClientConfig{}
	secOpts := &comm.SecureOptions{}
	if tlsRootBytes != nil {
		secOpts.UseTLS = true
		secOpts.ServerRootCAs = [][]byte{tlsRootBytes}
	}
	clientConfig.Timeout = 3 * time.Second
	clientConfig.SecOpts = secOpts
	gClient, err := comm.NewGRPCClient(clientConfig)
	if err != nil {
		return nil, err
	}
	return &peerClient{commonClient{
		GRPCClient: gClient,
		address:    address,
		sn:         sn,
	}}, nil
}

type peerClient struct {
	commonClient
}

func (pc *peerClient) Endorser() (pb.EndorserClient, error) {
	conn, err := pc.commonClient.NewConnection(pc.address, pc.sn)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("endorser client failed to connect to %s", pc.address))
	}
	return pb.NewEndorserClient(conn), nil
}

func (pc *peerClient) Deliver() (pb.Deliver_DeliverClient, error) {
	conn, err := pc.commonClient.NewConnection(pc.address, pc.sn)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("endorser client failed to connect to %s", pc.address))
	}
	return pb.NewDeliverClient(conn).Deliver(context.TODO())
}

// PeerDeliver returns a client for the Deliver service for peer-specific use
// cases (i.e. DeliverFiltered)
func (pc *peerClient) PeerDeliver() (api.PeerDeliverClient, error) {
	conn, err := pc.commonClient.NewConnection(pc.address, pc.sn)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("deliver client failed to connect to %s", pc.address))
	}
	pbClient := pb.NewDeliverClient(conn)
	return &common.PeerDeliverClient{Client: pbClient}, nil
}

// Admin returns a client for the Admin service
func (pc *peerClient) Admin() (pb.AdminClient, error) {
	conn, err := pc.commonClient.NewConnection(pc.address, pc.sn)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("admin client failed to connect to %s", pc.address))
	}
	return pb.NewAdminClient(conn), nil
}

// Certificate returns the TLS client certificate (if available)
func (pc *peerClient) Certificate() tls.Certificate {
	return pc.commonClient.Certificate()
}

func (pc *peerClient) GetDeliverClient(signer cryptoutil.Signer, channelID string, bestEffort bool) (*DeliverClient, error) {
	var tlsCertHash []byte
	deliver, err := pc.Deliver()
	if err != nil {
		return nil, err
	}

	// check for client certificate and create hash if present
	if len(pc.Certificate().Certificate) > 0 {
		tlsCertHash = util.ComputeSHA256(pc.Certificate().Certificate[0])
	}
	ds := &peerDeliverService{deliver}
	return &DeliverClient{
		Service: ds,
		TLSCertHash: tlsCertHash,
		ChannelID: channelID,
		BestEffort: bestEffort,
		signer: signer,
	}, nil
}

type peerDeliverService struct {
	pb.Deliver_DeliverClient
}

func (p *peerDeliverService) Recv() (*ab.DeliverResponse, error) {
	pbResp, err := p.Deliver_DeliverClient.Recv()
	if err != nil {
		return nil, errors.Wrap(err, "error receiving from peer deliver service")
	}

	abResp := &ab.DeliverResponse{}

	switch t := pbResp.Type.(type) {
	case *pb.DeliverResponse_Status:
		abResp.Type = &ab.DeliverResponse_Status{Status: t.Status}
	case *pb.DeliverResponse_Block:
		abResp.Type = &ab.DeliverResponse_Block{Block: t.Block}
	default:
		return nil, errors.Errorf("response error: unknown type %T", t)
	}

	return abResp, nil
}

